﻿using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.AccessControl;
using System.Security.Permissions;
using System.Security.Principal;
using System.Threading;
using JiShiBen.Core.DataStructure;
using JiShiBen.Core.Properties;
using Microsoft.Win32.SafeHandles;

namespace JiShiBen.Core
{
	[ComVisible(true)]
	public class LimitlessFileStream : Stream
	{
		#region Private members

		#region Constanst

		private const int DEFAULT_BUFFER_SIZE = 4096;
		private const int DEFAULT_SAVE_FRAGMENT_LENGTH = 0xFFFF;
		private const FileOptions DEFAULT_FILE_OPTIONS = FileOptions.Asynchronous | FileOptions.RandomAccess;
		const FileAccess DEFAULT_FILE_ACCESS = FileAccess.ReadWrite;
		private const string GUEST_FILE_NAME_PATTERN = "__@@{0}__";

		#endregion


		#region Fields

		private FileStream _host;
		private FileStream _guest;
		private FragmentList _fragments;
		private string _fileName;

		private long _length;
		private long _offset;

		#endregion


		#region Methods

		private void InitStream()
		{
			_host = new FileStream(Name, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, DEFAULT_BUFFER_SIZE,
								   false);
			_length = _host.Length;
			var fi = new FileInfo(Name);

			_guest = new FileStream(GetGuestFileName(Name), FileMode.Create, DEFAULT_FILE_ACCESS, FileShare.Read, 4096,
				DEFAULT_FILE_OPTIONS);
		}

		private string GetGuestFileName(string originalFileName)
		{
			var fi = new FileInfo(originalFileName);
			DirectoryInfo di;
			di = string.IsNullOrWhiteSpace(fi.DirectoryName)
				? new DirectoryInfo(Environment.CurrentDirectory)
				: new DirectoryInfo(fi.DirectoryName);

			if (!DirectoryIsWritable(di.FullName) || FileOnNonFixedDrive(di) || NotEnoughFreeSpaceOnDrive(di))
				di = new DirectoryInfo(Path.GetTempPath());

			var name = Path.GetFileNameWithoutExtension(originalFileName);
			name = string.Format(GUEST_FILE_NAME_PATTERN, name);
			return Path.Combine(di.FullName, name);
		}

		private bool NotEnoughFreeSpaceOnDrive(DirectoryInfo info)
		{
			var di = new DriveInfo(info.Root.FullName);

			return di.TotalFreeSpace < _length;
		}

		private static bool DirectoryIsWritable(string path)
		{
			//	Get access rules of the specifiied files (usser groups and user names that have access to the directory)
			var rules = Directory.GetAccessControl(path).GetAccessRules(true, true, typeof(SecurityIdentifier));

			//	Get the identity of the current user and the groups that the user is in
			var groups = WindowsIdentity.GetCurrent().Groups;
			var sidCurrentUser = WindowsIdentity.GetCurrent().User.Value;

			//	Check if writing to the file is explicitly denied for the user or a group
			if (rules.OfType<FileSystemAccessRule>().Any(r => (groups.Contains(r.IdentityReference)
				|| r.IdentityReference.Value == sidCurrentUser) && r.AccessControlType == AccessControlType.Deny
				&& (r.FileSystemRights & FileSystemRights.WriteData) == FileSystemRights.WriteData))
				return false;

			//	Check if writing is allowed
			return rules.OfType<FileSystemAccessRule>().Any(r => (groups.Contains(r.IdentityReference)
				|| r.IdentityReference.Value == sidCurrentUser) && r.AccessControlType == AccessControlType.Allow
				&& (r.FileSystemRights & FileSystemRights.WriteData) == FileSystemRights.WriteData);
		}

		private static bool FileOnNonFixedDrive(DirectoryInfo info)
		{
			var drives = DriveInfo.GetDrives();
			var drive = drives.FirstOrDefault(d => d.Name == info.Root.FullName);

			return drive == null || drive.DriveType != DriveType.Fixed;
		}

		private void InitializeFragments()
		{
			_fragments = new FragmentList();
			if (string.IsNullOrWhiteSpace(Name) || _host.Length <= 0) return;

			var fragment = new Fragment(HostType.Host, 0, _host.Length);
			_fragments.Insert(fragment);
		}

		private static string GetSaveStreamName(string originalFileName)
		{
			var fName = Path.GetFileNameWithoutExtension(originalFileName);
			var di = Path.GetDirectoryName(originalFileName);

			return Path.Combine(di, string.Format("$${0}.saving", fName));
		}


		#endregion

		#endregion


		#region Constructor

		[SecuritySafeCritical]
		public LimitlessFileStream(string path)
		{
			Init(path);
		}

		#endregion


		#region Protected members

		#region Properties
		#endregion

		#region Methods

		protected void Init(string path)
		{
			if (path == null)
				throw new ArgumentNullException("path");
			if (!File.Exists(path))
				__Error.FileNotFound(path);

			_offset = 0;
			_fileName = new FileInfo(path).FullName;

			InitStream();
			InitializeFragments();
		}

		protected virtual void SaveToDisk()
		{
			var fs = new FileStream(GetSaveStreamName(Name), FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite,
									4096, FileOptions.Asynchronous | FileOptions.RandomAccess | FileOptions.WriteThrough);

			var resetEvent = new ManualResetEvent(false);

		}

		protected virtual int ReadStreamAtOffset(byte[] buffer, int bufferOffset, long offset, int length)
		{
			if (buffer == null)
				throw new ArgumentNullException("buffer");
			if (buffer.Length == 0)
				throw new ArgumentException(Resources.IO_Argument_BufferLengthCannotZero);
			if (bufferOffset >= buffer.Length || bufferOffset + length >= buffer.Length)
				throw new ArgumentOutOfRangeException(Resources.IO_Argument_OutOfRange_ExceedCapacity);

			long adjustedOffset;
			int writeIndex = 0;
			int totalReadCount = 0;
			var fragment = _fragments.GetListItem(offset, out adjustedOffset);

			long remaining = length;
			while (remaining > 0 && fragment != null)
			{
				var tempLength = remaining;
				var fs = fragment.HostIndex == HostType.Host ? _host : _guest;
				if (fragment.LastIndex - offset < tempLength)
					tempLength = fragment.LastIndex - offset;

				var tempBuffer = new byte[tempLength];
				fs.Seek(adjustedOffset, SeekOrigin.Begin);
				var readCount = fs.Read(tempBuffer, 0, tempBuffer.Length);
				Array.Copy(tempBuffer, 0, buffer, writeIndex, readCount);

				totalReadCount += readCount;
				remaining -= readCount;
				adjustedOffset += readCount;
				writeIndex += readCount;
				if (adjustedOffset > fragment.LastIndex)
					fragment = fragment.Next;
			}

			return totalReadCount;
		}

		protected virtual void Write(byte[] buffer, int bufferOffset, long offset, int length)
		{
			
		}

		#endregion

		#endregion


		#region Public members

		#region Properties

		public string Name
		{
			get
			{
				return string.IsNullOrWhiteSpace(_fileName) ? Resources.IO_UnknownFileName : _fileName;
			}
		}

		#endregion


		#region Methods

		public int Read(byte[] buffer, int bufferOffset, long offset, int length)
		{
			return ReadStreamAtOffset(buffer, bufferOffset, offset, length);
		}

		#endregion

		#endregion


		#region Overrides

		#region Properties

		public override long Length
		{
			get { return _fragments.Length; }
		}

		public override bool CanRead
		{
			get { return _host.CanRead && _guest.CanRead; }
		}

		public override bool CanSeek
		{
			get { return _host.CanSeek && _guest.CanSeek; }
		}

		public override bool CanWrite
		{
			get { return _guest.CanWrite && _host.CanWrite; }
		}

		public override void Flush()
		{
			throw new NotImplementedException();
		}

		public override long Position
		{
			get
			{
				if (_host == null)
					throw new ObjectDisposedException(Resources.ObjectDisposed_FileClosed);
				if (!CanSeek)
					throw new NotSupportedException(Resources.NotSupported_UnseekableStream);

				return _offset;
			}
			set
			{
				if (value < 0L)
					throw new ArgumentOutOfRangeException("value", Resources.ArgumentOutOfRange_NeedNonNegNum);

				Seek(value, SeekOrigin.Begin);
			}
		}

		#endregion


		#region Methods

		public override int Read(byte[] buffer, int offset, int count)
		{
			var readCount = ReadStreamAtOffset(buffer, offset, _offset, count);
			_offset += readCount;

			return readCount;
		}

		public override long Seek(long offset, SeekOrigin origin)
		{
			switch (origin)
			{
				case SeekOrigin.Begin:
					_offset = offset;
					break;
				case SeekOrigin.Current:
					_offset += offset;
					break;
				default:
					_offset = _length + offset - 1;
					break;
			}

			if (_offset + 1 > _length)
			{
				var exceeded = (_offset + 1) - _length;
				_fragments.Insert(new Fragment(HostType.Working, _guest.Length, exceeded));
				_guest.Seek(exceeded, SeekOrigin.End);
				_guest.Flush();
			}

			return _offset;
		}

		public override void SetLength(long value)
		{
			
		}

		public override void Write(byte[] buffer, int offset, int count)
		{
			throw new NotImplementedException();
		}

		protected override void Dispose(bool disposing)
		{
			if (_host != null)
				_host.Dispose();
			if (_guest != null)
				_guest.Dispose();

			base.Dispose(disposing);
		}

		#endregion

		#endregion

	}
}
